OpenAI APIのFunction calling機能でGPTに検索結果に基づいた回答をさせてみる
こんちには。
データアナリティクス事業本部 インテグレーション部 機械学習チームの中村です。
本記事では、Function callingの応用として、SerpApiとBeautiful Soupを使い、GPT-3.5に検索結果に基づいた回答をさせて見たいと思います。
Function callingとは
Function callingは、自身で定義した処理を組み込んだチャットが実現できる機能となっています。
与えられたfunctionの引数や説明をOpenAI APIのチャットが確認して使用する関数、引数を決める形で実行されます。
そのため、OpenAI APIへのChatCompletionは複数回呼ばれるような仕組みです。
いくつか弊社のブログでも記事が出ておりますので、そちらもご参照ください。
- [OpenAI] Function callingで遊んでみたら本質が見えてきたのでまとめてみた | DevelopersIO
- OpenAI APIのFunction callingで関数が呼ばれる条件を確認してみた | DevelopersIO
- GPTのFunction callingを使って自然言語が新たなインターフェースになるかを試してみる | DevelopersIO
作ってみる
それでは実際にSerpApiとBeautiful Soupを使って作っていきたいともいます。
各種APIキーの準備
ここは他の記事でも言及がありますので、その記事の紹介にとどめます。
OpenAI APIキーについては以下をご参照ください。
SerpApiのAPIキーについては以下を参照ください。(無償版は検索回数が限られているのでご注意ください)
実行環境
Google Colaboratoryを使います。
!python --version
Python 3.10.12
必要なライブラリを入れておきます。
!pip install openai google-search-results
ライブラリのバージョンは以下となりました。
!pip freeze | grep -e "openai" -e "google-search-results"
google-search-results==2.4.2 openai==0.27.8
APIキーの設定
以下のように設定して置きます。
OPENAI_API_KEY="{skで始まるOPENAI APIキー}" SERPAPI_API_KEY="{SerpApiのAPIキー}"
OpenAI APIの単体サンプルコード
いつも通りの記述ですが、以下のようにChatCompletionを呼び出します。
import openai openai.api_key = OPENAI_API_KEY model_name = "gpt-3.5-turbo-16k-0613" question = "PythonでOpenAI APIを使う方法" response = openai.ChatCompletion.create( model=model_name, messages=[ {"role": "user", "content": question}, ], ) print(response.choices[0]["message"]["content"].strip())
SerpAPIの単体サンプルコード
以下のように、site:dev.classmethod.jp
を付けたクエリとすれば、クラスメソッドのブログ内に限定して検索ができます。
from serpapi import GoogleSearch query_str = "PythonでOpenAI APIを使う方法" search = GoogleSearch({ "q": f"site:dev.classmethod.jp {query_str}", "api_key": SERPAPI_API_KEY }) result = search.get_dict()
検索結果のURLのリストは、organic_results
のlink
を集めてくれば取得できます。
address_list = [result['link'] for result in result['organic_results']] address_list
['https://dev.classmethod.jp/articles/openai-api-chat-python-first-step/', 'https://dev.classmethod.jp/articles/first_step-openai_api/', 'https://dev.classmethod.jp/articles/openai-api-quickstart-tutorial/', 'https://dev.classmethod.jp/articles/openai-create-images/', 'https://dev.classmethod.jp/articles/azure-openai-from-python-module/', 'https://dev.classmethod.jp/articles/understand-openai-function-calling/', 'https://dev.classmethod.jp/articles/888c355f2c88e117d172ec1bd3d28a435ee438766630638e3e9f7887aef8f5ee/', 'https://dev.classmethod.jp/articles/search-with-openai-embeddings/', 'https://dev.classmethod.jp/articles/open-api-lambda-test/', 'https://dev.classmethod.jp/articles/tried-the-code-in-the-openai-cookbook/']
SerpApiの使い方の詳しい説明は以下も参照下さい。
Beautiful Soupの単体サンプルコード
urllibでHTMLを取得して、Beautiful Soupでパースします。
import urllib.request from bs4 import BeautifulSoup # HTMLを取得 req = urllib.request.Request(address_list[0]) with urllib.request.urlopen(req) as res: body = res.read() html_doc = body.decode() # パース処理 soup = BeautifulSoup(html_doc, 'html.parser') contents = soup.find('div', class_="content") texts = [c.get_text() for c in contents.find_all('p')] texts = "\n\n".join(texts) print(texts[:4000])
HTMLのパース処理は、クラスメソッドのブログのHTML構成に合わせています。
別のソースを使用する場合、この部分はそのまま使用できないので注意が必要です。
ここはLlamaIndexなどで実装されているParserを使うのも手だと思いますが、今回は自作しています。
またトークン数が多くなることを防ぐために、文字数も4000文字に制限しています。
Function callingに使用するユーザ関数の定義
それぞれの単体サンプルコードを元にして、Function callingに使用するユーザ関数の定義していきます。
まずはSerpApiを使って検索するユーザ関数を以下のように定義します。
from serpapi import GoogleSearch def search_cm_blog(query: str) -> str: search = GoogleSearch({ "q": f"site:dev.classmethod.jp {query_str}", "api_key": SERPAPI_API_KEY }) result = search.get_dict() address_list = [result['link'] for result in result['organic_results']] address_list return str(address_list)
検索型のポイントは、得られるURLのリストを文字列化している点です。
これはFunction callingの結果をChatCompletionのmessage履歴に含める際に、そのままlist型のデータが使用できないため、このような実装となっています。
次に、URLのデータを取得するユーザ関数を以下のように定義します。
import urllib.request from bs4 import BeautifulSoup def get_blog_contents(url: str) -> str: req = urllib.request.Request(url) with urllib.request.urlopen(req) as res: body = res.read() html_doc = body.decode() soup = BeautifulSoup(html_doc, 'html.parser') contents = soup.find('div', class_="content") texts = [c.get_text() for c in contents.find_all('p')] texts = "\n\n".join(texts) return texts[:4000]
こちらについては、ほぼサンプルコードのままとなっています。
Function callingの設定
OpenAI APIに渡すfunctions
を以下のように定義して、Function callingを使用できるようにします。
functions=[ { "name": "search_cm_blog", "description": "指定したキーワードでクラスメソッドのブログを検索して、URLのリストを得る。", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "検索キーワード", }, }, "required": ["query"], }, }, { "name": "get_blog_contents", "description": "指定したURLについてその内容を取得して、パースした結果のテキストを得る。", "parameters": { "type": "object", "properties": { "url": { "type": "string", "description": "内容を取得したいページのURL", }, }, "required": ["url"], }, } ]
なるべく詳しく書くように努力しています。
Function callingを使用したOpenAI API呼び出し
最後に、Function callingを使用したOpenAI API呼び出しを以下のように記述しました。
少し長いですが、後程補足します。
import openai openai.api_key = OPENAI_API_KEY model_name = "gpt-3.5-turbo-16k-0613" query_str = "PythonでOpenAI APIを使う方法" question = f""" 「{query_str}」について、まずクラスメソッドのブログを検索した結果のその上位3件を取得します。 その後、それぞれのURLについてその内容を取得して、パースした結果のテキスト得ます。 そしてそれらのパースした結果をまとめ、最終的な答えを1つ生成してください。 """ MAX_REQUEST_COUNT = 10 message_history = [] for request_count in range(MAX_REQUEST_COUNT): function_call_mode = "auto" if request_count == MAX_REQUEST_COUNT - 1: function_call_mode = "none" response = openai.ChatCompletion.create( model=model_name, messages=[ {"role": "user", "content": question}, *message_history, ], functions=functions, function_call=function_call_mode, ) if response["choices"][0]["message"].get("function_call"): print("*** Function calling ***") print(response["choices"][0]["message"].get("function_call")) message = response["choices"][0]["message"] message_history.append(message) function_call = response["choices"][0]["message"].get("function_call") function_name = function_call.get("name") if function_name in [f["name"] for f in functions]: function_arguments = function_call.get("arguments") function_response = eval(function_name)(**eval(function_arguments)) else: raise Exception message = { "role": "function", "name": function_name, "content": function_response, } message_history.append(message) else: print("*** Final answer ***") print(response.choices[0]["message"]["content"].strip()) break
実行結果は以下のようになります。
*** Function calling *** { "name": "search_cm_blog", "arguments": "{\n \"query\": \"Python OpenAI API\"\n}" } *** Function calling *** { "name": "get_blog_contents", "arguments": "{\n \"url\": \"https://dev.classmethod.jp/articles/openai-api-chat-python-first-step/\"\n}" } *** Function calling *** { "name": "get_blog_contents", "arguments": "{\n \"url\": \"https://dev.classmethod.jp/articles/first_step-openai_api/\"\n}" } *** Function calling *** { "name": "get_blog_contents", "arguments": "{\n \"url\": \"https://dev.classmethod.jp/articles/openai-api-quickstart-tutorial/\"\n}" } *** Final answer *** 以下はクラスメソッドのブログ記事についてのまとめです: 1. [OpenAI APIを使ってPythonでChatGPTを使う超基本的な使い方](https://dev.classmethod.jp/articles/openai-api-chat-python-first-step/) この記事では、PythonでChatGPTを使うための超基本的な使い方が紹介されています。APIキーの発行方法から始まり、APIのインストールや環境変数の設定、サンプルコードの解説まで詳しく説明されています。 2. [OpenAI APIを使ってChatGPTを導入する方法](https://dev.classmethod.jp/articles/first_step-openai_api/) この記事では、OpenAI APIを使ってChatGPTを導入する方法が解説されています。APIキーの取得方法やデモ用コードの使い方、実際にChatGPTを使って動物の名前を生成するアプリケーションの作り方などが説明されています。 3. [OpenAI API Quickstart Tutorialをなぞってみた](https://dev.classmethod.jp/articles/openai-api-quickstart-tutorial/) この記事では、OpenAI APIのQuickstartチュートリアルをなぞった結果が紹介されています。チュートリアルの進め方やAPIキーの取得方法、プロンプトの指定方法などが詳しく解説されています。 以上の記事を参考にすると、PythonでOpenAI APIを使ってChatGPTを導入し、基本的な使い方を理解することができます。
このように、最初にsearch_cm_blog
を呼び出し、その後get_blog_contents
を3回呼び出し、最終的な結果を得る想定通りの動作をしてくれることが分かりました。
複雑なFunction callingの使い方が良く分かった気がします。
実装のポイント1 : evalを使って定義した関数を呼ぶ
使用したい関数は、OpenAI APIに決めてもらいますが、その結果は全て文字列となっていますので、eval()
をうまく使って関数を呼び出します。
function_call = response["choices"][0]["message"].get("function_call") function_name = function_call.get("name") if function_name in [f["name"] for f in functions]: function_arguments = function_call.get("arguments") function_response = eval(function_name)(**eval(function_arguments)) else: raise Exception
まずeval(function_name)
を使って関数を実体化させます。
次に引数も文字列となっているのでeval(function_arguments)
で実体化し、また引数の種類は関数によって異なることから、得られたdict型を**
で展開(アンパック)して関数に与えています。
なお、念のためfunctionsにない関数が呼ばれないように例外処理を設けています。
実装のポイント2 : 最大のリクエスト回数を決めておく
function_callをauto
とすると、ずっと処理が終わらない可能性があるので、最大のリクエスト回数を決めています。
最大のリクエスト回数を超過しそうになると、function_callをnone
に設定してそこまでで処理を終わらせます。
for request_count in range(MAX_REQUEST_COUNT): function_call_mode = "auto" if request_count == MAX_REQUEST_COUNT - 1: function_call_mode = "none"
実装のポイント3 : 途中のレスポンスとfunctionの実行結果は履歴として与える
それまでのやり取りをmessage_history
に格納しておくことで、続きの結果を得ることができます。
ですので、そこまでの途中のレスポンスに加えて、functionの実行結果も履歴として格納しておきます。
if response["choices"][0]["message"].get("function_call"): print("*** Function calling ***") print(response["choices"][0]["message"].get("function_call")) message = response["choices"][0]["message"] message_history.append(message) message = { "role": "function", "name": function_name, "content": function_response, } message_history.append(message)
functionの実行結果は、"role": "function"
という形でOpenAI APIでは見分けているようですね。
まとめ
いかがでしたでしょうか。
OpenAI APIのFunction callingの応用例についてご紹介致しました!
複数のfunctionがある場合でもうまく動作させることができました。本記事がFunction callingを使用される方の参考になれば幸いです。